/**
 * 
 */
package playGround.dungeon;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import playGround.square.Square;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;
import exceptions.IllegalPositionException;


/**
 * @author Niels en Maarten
 * @version 2.3
 * @invar 
 * 		| areValidSubDungeons()
 */
public class CompositeDungeon<S extends Square> extends Dungeon<S>{
	/**
	 * the subDungeons of this composite dungeon.
	 */
	private Map<Position,Dungeon<? extends S>> subDungeons;
	/**
	 * @param maxDimensions of the compositeDungeon
	 * @effect 
	 * 		| super(maxDimensions)
	 * @post		
	 * 		| this.getSubDungeons() = new HashMap<Position,Dungeon<? extends S>>()
	 * 		
	 */
	@Raw
	public CompositeDungeon(Position maxDimensions) {
		super(maxDimensions);
		subDungeons=new HashMap<Position,Dungeon<? extends S>>();
	}
	/**
	 * 
	 * @param dungeon you want to add to this dungeon
	 * @param positionOrigin the distance between the 2 points of origin
	 * @throws IllegalArgumentException if the subDungeon isn't a valid one
	 * 		| ! isValidSubDungeon(dungeon, positionOrigin)
	 * @post
	 * 		| new.getSubDungeons().contains(positionOrigin)
	 * 		| new.getSubDungeons().get(positionOrigin) == dungeon
	 * 		| new.getSubDungeons().size() == this.getSubDungeons().size() +1
	 * 		| dungeon.setSuperDungeon(this)
	 * @post
	 * 		| dungeon.getSuperDungeon() == this
	 */
	public void addSubDungeon(Dungeon<? extends S> dungeon, Position positionOrigin) throws IllegalArgumentException{
		if(!isValidSubDungeon(dungeon, positionOrigin))
			throw new IllegalArgumentException();
		subDungeons.put(positionOrigin, dungeon);
		dungeon.setSuperDungeon(this);
		for(Dungeon<? extends S> shaftOrLevel: dungeon.getShaftsAndLevels()){
			Map<Position, Square> positionSquares=dungeon.getPositionSquares();
			NonCompositeDungeon<? extends S> subDungeon=((NonCompositeDungeon<? extends S>) shaftOrLevel);
			for(Position position: positionSquares.keySet()){
				subDungeon.resolveAreaAndBorderConflict(position, positionSquares.get(position));
				if(!subDungeon.canHaveAsPosition(position, null))
					throw new IllegalPositionException();
			}
		}
	}
	/**
	 * this method checks whether or not the given position is valid for dungeon.
	 * @param position: the position for the dungeon
	 * @param dungeon: the dungeon in which the given position is specified
	 * @return
	 * 		| Position absolute= this.getAbsoluteCoordinates(this.getPositionOrigin(dungeon), position)
	 * 		| if(absolute.getXValue()==absolute.getYValue()&&absolute.getYValue()==absolute.getZValue())
	 * 		| result == false
	 * @return	
	 * 		| if(this.getSuperDungeon()!=null && this.getSuperDungeon().canHaveAsPosition(position))
	 * 		| result == true
	 */
	@Override
	public boolean canHaveAsPosition(Position position, Dungeon<? extends S> dungeon){
		Position absolute= this.getAbsoluteCoordinates(this.getPositionOrigin(dungeon), position);
		boolean check=true;
		if(absolute.getXValue()==absolute.getYValue()&&absolute.getYValue()==absolute.getZValue())
			check= false;
		if(this.getSuperDungeon()==null)
			return check;
		return check&&this.getSuperDungeon().canHaveAsPosition(position);

	}
	/**
	 * @param subdungeon you want to check
	 * @param positionOrigin compared to the origin of this dungeon
	 * @return
	 * 		| if(super.getMaxDimensions().getXValue()<this.getAbsoluteCoordinates(positionOrigin, dungeon.getMaxDimensions()).getXValue())
	 * 		| then result == false
	 * @return
	 * 		|if(super.getMaxDimensions().getYValue()<this.getAbsoluteCoordinates(positionOrigin, dungeon.getMaxDimensions()).getYValue())
	 * 		| then result == false
	 * @return		
	 * 		| if(super.getMaxDimensions().getZValue()<this.getAbsoluteCoordinates(positionOrigin, dungeon.getMaxDimensions()).getZValue())
	 * 		| then result == false
	 * @return
	 * 		| if(! checkOverlapping(dungeon,positionOrigin))
	 * 		| then result == false
	 */
	public boolean isValidSubDungeon(Dungeon<? extends S> dungeon, Position positionOrigin){
		Position maxDimensionsSuperDungeon =super.getMaxDimensions();
		Position absoluteMaxDimensionsDungeon=getAbsoluteCoordinates(positionOrigin, dungeon.getMaxDimensions());
		if(maxDimensionsSuperDungeon.getXValue()<absoluteMaxDimensionsDungeon.getXValue())
			return false;
		if(maxDimensionsSuperDungeon.getYValue()<absoluteMaxDimensionsDungeon.getYValue())
			return false;
		if(maxDimensionsSuperDungeon.getZValue()<absoluteMaxDimensionsDungeon.getZValue())
			return false;
		return checkOverlapping(dungeon,positionOrigin);
		
	}
	/**
	 * 
	 * @return
	 * 		| for each position in getSubDungeons().keySet()
	 * 		| this.removeSubDungeonAtPosition(position)
	 * 		| if(! isValidSubDungeon(getSubDungeons().get(position),position))
	 * 		| then result == false
	 * 		| this.addSubDungeon(dungeon,position)
	 */
	public boolean areValidSubDungeons(){
		for(Position position : getSubDungeons().keySet()){
			Dungeon<? extends S> dungeon = getSubDungeons().get(position);
			removeSubDungeonAtPosition(position);
			if(! isValidSubDungeon(dungeon,position))
				return false;
			addSubDungeon(dungeon,position);
		}
		return true;
	}
	/**
	 * 
	 * @param dungeon you want to check 
	 * @param positionOrigin compared to the origin of this dungeon
	 * @return
	 * 		| for each position in getSubDungeons().keySet()
	 * 		| subDungeon = this.getSubDungeons().get(position)
	 * 		| for each existingPosition in this.getAllAbsolutePositionsOfSubDungeon(subDungeon,position)
	 * 		| if(this.getAllAbsolutePositionsOfSubDungeon(dungeon,positionOrigin).contains(existingPosition))
	 * 		| result == false
	 */
	public boolean checkOverlapping(Dungeon<? extends S> dungeon, Position positionOrigin){
		Set<Position> newPositions=getAllAbsolutePositionsOfSubDungeon(dungeon,positionOrigin);
		for(Position position:subDungeons.keySet()){
			Dungeon<? extends S> existingSubDungeon=subDungeons.get(position);
			for(Position existingPosition: getAllAbsolutePositionsOfSubDungeon(existingSubDungeon,position)){
				if(newPositions.contains(existingPosition))
					return false;
			}			
		}
		return true;
	}
	/**
	 * This method returns the absolute coordinate
	 * @param positionOrigin the position of the origin of the dungeon
	 * @param relativePosition the relative position of the dungeon
	 * @return
	 * 		| result == new Position(positionOrigin.getXValue()+relativePosition.getXValue(),positionOrigin.getYValue()+relativePosition.getYValue(),positionOrigin.getZValue()+relativePosition.getZValue())
	 */
	public Position getAbsoluteCoordinates(Position positionOrigin, Position relativePosition){
		return new Position(positionOrigin.getXValue()+relativePosition.getXValue(),positionOrigin.getYValue()+relativePosition.getYValue(),positionOrigin.getZValue()+relativePosition.getZValue());
	}
	/**
	 * 
	 * @param positionOrigin the position of the origin of the dungeon
	 * @param absolutePosition the relative position of the dungeon
	 * @return
	 * 		| result == new Position(absolutePosition.getXValue()-positionOrigin.getXValue(),absolutePosition.getYValue()-positionOrigin.getYValue(),absolutePosition.getZValue()-positionOrigin.getZValue())
	 */
	public Position getRelativeCoordinates(Position positionOrigin, Position absolutePosition){
		return new Position(absolutePosition.getXValue()-positionOrigin.getXValue(),absolutePosition.getYValue()-positionOrigin.getYValue(),absolutePosition.getZValue()-positionOrigin.getZValue());
	}
	/**
	 * This method gets the subDungeons of the dungeon
	 * @return the subdungeons of this composite dungeon
	 */
	@Basic
	public Map<Position,Dungeon<? extends S>> getSubDungeons(){
		return new HashMap<Position,Dungeon<? extends S>>(this.subDungeons);
	}
	/**
	 * This method removes a subDungeon from the dungeon
	 * @param dungeon you want to remove
	 * @post ...
	 * 		| ! (new.getSubDungeons().contains(dungeon))
	 * 		| new.getSubDungeons().size() = this.getSubDugeons().size()-1
	 * @effect ...
	 * 		| dungeon.terminate()
	 * @throws IllegalArgumentException
	 * 		| this.getPositionOrigin(dungeon)==null
	 */
	public void removeSubDungeon(Dungeon<? extends S> dungeon)throws IllegalArgumentException{
		Position positionOrigin=getPositionOrigin(dungeon);
		if(positionOrigin==null)
			throw new IllegalArgumentException();
		this.subDungeons.remove(positionOrigin);
		dungeon.terminate();
	}
	/**
	 * This method gets all the positions of the subDungeon
	 * @param dungeon the dungeon you want to investigate
	 * @param positionOrigin of the subDungeon
	 * @return
	 * 		| Set<Position> positionSet=new HashSet<Position>()
	 * 		| for every x: x<=dungeon.getMaxDimensions().getXValue()
	 * 		| 	for every y: y<=dungeon.getMaxDimensions().getYValue()
	 * 		| 		for every z: z<=dungeon.getMaxDimensions().getZValue()
	 * 		| 			positionSet.add(getAbsoluteCoordinates(positionOrigin, new Position(x,y,z)))
	 * 		| result == positionSet
	 */
	public Set<Position> getAllAbsolutePositionsOfSubDungeon(Dungeon<? extends S> dungeon, Position positionOrigin){
		Set<Position> result=new HashSet<Position>();
		for(long x=0;x<=dungeon.getMaxDimensions().getXValue();x++){
			for(long y=0;y<=dungeon.getMaxDimensions().getYValue();y++){
				for(long z=0;z<=dungeon.getMaxDimensions().getZValue();z++)
					result.add(getAbsoluteCoordinates(positionOrigin, new Position(x,y,z)));
			}
		}
		return result;
	}
	/**
	 * This method removes a subDungeon from a position
	 * @param positionOrigin the origin of the subDungeon you want to remove
	 * @post
	 * 		| ! (new.getSubDungeons().contains(dungeon))
	 * 		| new.getSubDungeons().size() = getSubDugeons().size()-1
	 * @post
	 * 		| new.getSubDungeons().get(positionOrigin).getSuperDungeon()==null
	 */
	public void removeSubDungeonAtPosition(Position positionOrigin){
		this.subDungeons.remove(positionOrigin);
		getSubDungeons().get(positionOrigin).setSuperDungeon(null);
	}
	/**
	 * This method gets the shafts and levels from a dungeon
	 * @return		
	 * 		| Set<Dungeon<? extends S>> dungeonSet=new HashSet<Dungeon<? extends S>>()
	 * 		| for every dungeon in getSubDungeons().values()
	 * 		| if (dungeon instanceof NonCompositeDungeon)
	 * 		|	dungeonSet.add(dungeon)
	 * 		| else
	 * 		|	repeat for dungeon
	 * 		| result == dungeonSet
	 */
	public Set<Dungeon<? extends S>> getShaftsAndLevels(){
		Set<Dungeon<? extends S>> result=new HashSet<Dungeon<? extends S>>();
		for(Dungeon<? extends S> subDungeon:this.subDungeons.values())
			result.addAll(subDungeon.getShaftsAndLevels());
		return result;
	}
	/**
	 * This method controls if it has the subDungeon
	 * @param dungeon you want to check whether is is a subDungeon
	 * @return
	 * 		| result == (this.getSubDungeons().containsValue(dungeon)
	 */
	public boolean containsSubDungeon(Dungeon<? extends S> dungeon){
		return (this.subDungeons.containsValue(dungeon));
	}
	/**
	 * This method gets a subDungeon at a position
	 * @param position you want to get the subDungeon
	 * @return
	 * 		| for each positionOrigin in this.getSubDungeons().keySet()
	 * 		| if(this.getAllAbsolutePositionsOfSubDungeon(this.getSubDungeons().get(positionOrigin), positionOrigin).contains(position))
	 * 		| result == positionOrigin
	 * 		| else
	 * 		| result == null
	 */
	public Position getPositionOriginOfSubDungeonAtPosition(Position position){
		for(Position positionOrigin:this.getSubDungeons().keySet()){
			if(getAllAbsolutePositionsOfSubDungeon(this.getSubDungeons().get(positionOrigin), positionOrigin).contains(position))
				return positionOrigin;
		}
		return null;
	}
	/**
	 * This method removes a square from a position
	 * @param position: on which you want to remove the square
	 * @effect
	 * 		|getSubDungeonAtPosition(positionToBeRemoved).removeSquare(positionToBeRemoved)
	 * @throws	IllegalArgumentException
	 * 		|getPositionOriginOfSubDungeonAtPosition(positionToBeRemoved)==null
	 */
	@Override
	public void removeSquare(Position positionToBeRemoved) {
		Position positionOfOriginOfSubDungeon=getPositionOriginOfSubDungeonAtPosition(positionToBeRemoved);
		if(positionOfOriginOfSubDungeon==null)
			throw new IllegalArgumentException();
		this.getSubDungeons().get(positionOfOriginOfSubDungeon).removeSquare(getRelativeCoordinates(positionOfOriginOfSubDungeon, positionToBeRemoved));
	}
	/**
	 * this method gives back the position of all the squares in a map.
	 * @return
	 * 		|Map<Position,Square> positionSquares=new HashMap<Position,Square>()
	 * 		|for each positionOrigin in this.getSubDungeons().keySet()
	 * 		|	for each position in this.getSubDungeons().get(positionOrigin).getPositionSquares()
	 * 		|positionSquares.put(getAbsoluteCoordinates(positionOrigin, position), this.getSubDungeons().get(positionOrigin).getPositionSquares().get(position))
	 * 		| result == positionSquares
	 */
	@Override
	public Map<Position,Square> getPositionSquares(){
		Map<Position,Square> result=new HashMap<Position,Square>();
		for(Position positionOrigin:this.getSubDungeons().keySet()){
			Dungeon<? extends S> subDungeon= this.getSubDungeons().get(positionOrigin);
			Map<Position, Square> positionSquares = subDungeon.getPositionSquares();
			for(Position position: positionSquares.keySet()){
				result.put(getAbsoluteCoordinates(positionOrigin, position), positionSquares.get(position));
			}
		}
		return result;
	}
	/**
	 * This method returns an iterator for squares of the dungeon
	 */
	@Override
	public Iterator<Object> getSquares() {
		Iterator<Object> squares = new Iterator<Object>(){
			{
				nbDungeons = getSubDungeons().size();
				dungeonDone = 0;
				dungeons = new ArrayList<Dungeon<? extends S>>(getSubDungeons().values());
			}
			private Iterator<Object> subDungeon;
			private ArrayList<Dungeon<? extends S>> dungeons;
			private int nbDungeons;
			private int dungeonDone;
			private boolean initialize;
			@Override
			public boolean hasNext() {
				return ((dungeonDone) < nbDungeons && initialize&& subDungeon.hasNext()) || (dungeonDone+1 < nbDungeons) || ((dungeonDone) < nbDungeons && dungeons.get(dungeonDone).getSquares().hasNext() && !initialize) ;
			}

			@Override
			public Object next() throws NoSuchElementException {
				if( ! initialize){
					subDungeon = dungeons.get(dungeonDone).getSquares();
					initialize = true;
				}
				if(! hasNext())
					throw new NoSuchElementException();
				if(subDungeon.hasNext())
					return subDungeon.next();
				dungeonDone++;
				subDungeon = dungeons.get(dungeonDone).getSquares();
				return subDungeon;
			}
			@Override
			public void remove() {	
			}
		};
		int count = 0;
		if(count == 0)
			count++;
		else if(squares.hasNext() && count == 1)
			squares.next();
		return squares;
	}
	/**
	 * 
	 * @param dungeon the dungeon you want the position of the origin of
	 * @return 
	 * 			|for each position in this.getSubDungeons().keySet()
	 * 			|	if(dungeon == this.getSubDungeons().get(position))
	 * 			|		result==position
	 * @return	|if(!this.getSubDungeons().containsValue(dungeon))
	 * 			| result==null
	 */
	public Position getPositionOrigin(Dungeon<? extends S> dungeon){
		for(Position position:this.subDungeons.keySet()){
			if(dungeon==subDungeons.get(position))
				return position;
		}
		return null;
	}
	/**
	 * This method returns the absolute position 
	 * @param position you want to investigate
	 * @param dungeon where the position is situated
	 * @return	
	 * 		|Position absoluteCoordinates = this.getAbsoluteCoordinates(this.getPositionOrigin(dungeon), position)
	 * 		| if(getSuperDungeon()!=null)
	 * 		| then result == super.getSuperDungeon().getAbsolutePosition(absoluteCoordinates,this)
	 * 		| else
	 * 		| then result ==absoluteCoordinates
	 */
	@Override
	public Position getAbsolutePosition(Position position,Dungeon<? extends S> dungeon) {
		Position absoluteCoordinates = getAbsoluteCoordinates(getPositionOrigin(dungeon), position);
		if(super.getSuperDungeon()==null) {
			return absoluteCoordinates;
		}
		return super.getSuperDungeon().getAbsolutePosition(absoluteCoordinates, this);
	}
	/**
	 * @effect |super.terminate()
	 * @post	|new.getSubDungeons().isEmpty()==true
	 * @post	|for each dungeon in this.getSubDungeons().values()
	 * 			|	dungeon.terminate()
	 * @post	|
	 */
	@Override
	public void terminate(){
		super.terminate();
		for(Dungeon<? extends S> dungeon: this.getSubDungeons().values())
			dungeon.terminate();
		this.subDungeons.clear();
	}
}


